import time
from datetime import datetime
from typing import Optional, Callable

# edit here
USERNAME: Optional[str] = None
PASSWORD: Optional[str] = None

START_TIME: float = datetime(2024, 1, 1).timestamp() # unix timestamp
END_TIME: Optional[float] = None # None means today
USER_ID: Optional[str] = None # type user id string, get all user's messages if None

OUTPUT_PATH: Optional[str] = None
# -------------------------------------------------------------------------------------- #

if not isinstance(USERNAME, (str, type(None))) \
        or not isinstance(PASSWORD, (str, type(None))) \
        or not isinstance(START_TIME, float) \
        or not isinstance(END_TIME, (float, type(None))) \
        or not isinstance(USER_ID, (str, type(None))) \
        or not isinstance(OUTPUT_PATH, (str, type(None))):
    raise TypeError


if END_TIME is None:
    END_TIME = time.time()

if START_TIME >= END_TIME:
    raise ValueError

import os
import copy
import atexit
import threading
import colorama
from physicsLab import web
from concurrent.futures import ThreadPoolExecutor

colorama.init(autoreset=True)

user = web.User(
    USERNAME, PASSWORD
)
# you can also do like this
# user = web.User(
#    token= , auth_code=
# )

_output_cache: str = ""

def _print(message: str, *, end: str = "\n"):
    global _output_cache
    if OUTPUT_PATH is not None:
        _output_cache += message[5:] + end
    else:
        print(message, end=end)

def _exit_callback():
    if OUTPUT_PATH is not None:
        with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
            f.write(_output_cache)
            print(f"write messages to {OUTPUT_PATH}")

class Banned: # only a namespace
    TAKE_MESSAGES_AMOUNT = 20
    baned_template: Optional[dict] = None
    baned_messages = []
    IS_END: bool = False

    banned_message_callback: Optional[Callable] = None

    def __new__(cls):
        return cls

    @classmethod
    def _fetch_banned_template(cls) -> None:
        response = user.get_messages(5, take=1, no_templates=False)["Data"]

        for template in response["Templates"]:
            if template["Identifier"] == "User-Banned-Record":
                cls.baned_template = copy.deepcopy(template)
                break
        assert cls.baned_template is not None, "internal error, please bug report"

    @classmethod
    def _fetch_banned_messages(cls, skip: int):
        assert skip >= 0, "internal error, please bug report"
        assert END_TIME is not None, "internal error, please bug report"

        messages = user.get_messages(
            5, skip=skip * cls.TAKE_MESSAGES_AMOUNT, take=cls.TAKE_MESSAGES_AMOUNT,
        )["Data"]["Messages"]

        assert cls.baned_template is not None, "internal error, please bug report"

        for message in messages:
            if message["TimestampInitial"] < START_TIME * 1000:
                cls.IS_END = True
                break
            if START_TIME * 1000 <= message["TimestampInitial"] <= END_TIME * 1000:
                if (USER_ID is None or USER_ID == message["Users"][0]) \
                        and message["TemplateID"] == cls.baned_template["ID"]:
                    message = copy.deepcopy(message)
                    cls.baned_messages.append(message)
                    if cls.banned_message_callback is not None:
                        cls.banned_message_callback(message)

class Warned:
    TAKE_MESSAGE_AMOUNT = 20
    warned_messages = []
    IS_END = False
    warned_message_callback: Optional[Callable] = None
    maybe_warned_message_callback: Optional[Callable] = None

    def __new__(cls):
        return Warned

    @classmethod
    def _fetch_warned_messages(cls, user_id: str, skip: int) -> int:
        assert END_TIME is not None, "internal error, please bug report"

        comments = user.get_comments(
            user_id, "User", skip=skip, take=cls.TAKE_MESSAGE_AMOUNT
        )["Data"]["Comments"]

        if len(comments) == 0:
            cls.IS_END = True
            return -1

        for comment in comments:
            if comment["Timestamp"] < START_TIME * 1000:
                cls.IS_END = True
                break

            if START_TIME * 1000 <= comment["Timestamp"] <= END_TIME * 1000:
                if comment["Flags"] is not None \
                        and "Locked" in comment["Flags"] \
                        and "Reminder" in comment["Flags"] \
                        and comment not in cls.warned_messages:
                    comment = copy.deepcopy(comment)
                    cls.warned_messages.append(comment)
                    if cls.warned_message_callback is not None:
                        cls.warned_message_callback(comment)
                elif "警告" in comment["Content"]:
                    if cls.maybe_warned_message_callback is not None:
                        cls.maybe_warned_message_callback(comment)
        return comments[-1]["Timestamp"]

callback_lock = threading.Lock()
def banned_message_callback(banned_message: dict):
    callback_lock.acquire()
    _print(colorama.Fore.RED + "* BANNED MESSAGE", end="")
    _print(
        f": in \"{datetime.fromtimestamp(banned_message['TimestampInitial'] / 1000)}\", "
        f"Editor \"{banned_message['Fields']['Editor']}\" banned user "
        f"\"{banned_message['UserNames'][0]}({banned_message['Users'][0]})\" "
        f"until \"{banned_message['Fields']['Until']}\" "
        f"with content \"{banned_message['Fields']['Content']}\"",
    )
    callback_lock.release()

def maybe_warned_message_callback(message: dict):
    callback_lock.acquire()
    _print(colorama.Fore.YELLOW + "[[Maybe]] WARNED MESSAGE", end="")
    _print(
        f": in \"{datetime.fromtimestamp(message['Timestamp'] / 1000)}\", "
        f"\"{message['Nickname']}({message['UserID']})\" warned "
        f"with contents \"{message['Content']}\"",
    )
    callback_lock.release()

def warned_message_callback(warned_message: dict):
    callback_lock.acquire()
    _print(colorama.Fore.YELLOW + "* WARNED MESSAGE", end="")
    _print(
        f": in \"{datetime.fromtimestamp(warned_message['Timestamp'] / 1000)}\", "
        f"\"{warned_message['Nickname']}({warned_message['UserID']})\" warned "
        f"with contents \"{warned_message['Content'][15:-8]}\"",
    )
    callback_lock.release()

def _banned_main():
    FETCH_AMOUNT = 100
    counter: int = 0
    while not Banned.IS_END:
        with ThreadPoolExecutor(max_workers=FETCH_AMOUNT + 50) as executor:
            for i in range(FETCH_AMOUNT):
                executor.submit(
                    Banned._fetch_banned_messages, i + counter * FETCH_AMOUNT,
                )
        counter += 1

def _warned_main():
    assert USER_ID is not None, "internal error, please bug report"
    assert END_TIME is not None, "internal error, please bug report"
    counter2 = 0
    fetch_end_time = int(END_TIME * 1000)
    while not Warned.IS_END:
        fetch_end_time = Warned._fetch_warned_messages(USER_ID, fetch_end_time)

        counter2 += 1

def main():
    Banned.banned_message_callback = banned_message_callback
    Warned.warned_message_callback = warned_message_callback
    Warned.maybe_warned_message_callback = maybe_warned_message_callback
    Banned._fetch_banned_template()

    if USER_ID is None:
        print(colorama.Fore.RED + "Can NOT get all user's warned-message")
    if user.is_anonymous:
        print(colorama.Fore.RED + "must login to get warned message")

    _banned_main()
    if USER_ID is not None and not user.is_anonymous:
        _warned_main()

    print(f"banned amount : {colorama.Fore.RED}{len(Banned.baned_messages)}")
    if USER_ID is not None and not user.is_anonymous:
        print(f"warned amount : {colorama.Fore.YELLOW}{len(Warned.warned_messages)}")

class Timer:
    def __enter__(self):
        self.start = time.time()

    def __exit__(self, exc_type, exc_val, traceback):
        if exc_type is None:
            self.end = time.time()
            print("time:", self.end - self.start)

if __name__ == "__main__":
    atexit.register(_exit_callback)
    if OUTPUT_PATH is not None:
        OUTPUT_PATH = os.path.abspath(OUTPUT_PATH)
    print("program start, it may take a while")
    with Timer():
        main()
